/*
 * Copyright 2022-present Open Networking Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package bjtu.pinet;

import org.onlab.packet.*;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.net.*;
import org.onosproject.net.device.DeviceEvent;
import org.onosproject.net.device.DeviceListener;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.flow.*;
import org.onosproject.net.flow.criteria.PiCriterion;
import org.onosproject.net.host.HostService;
import org.onosproject.net.packet.*;
import org.onosproject.net.pi.model.PiActionId;
import org.onosproject.net.pi.model.PiActionParamId;
import org.onosproject.net.pi.model.PiMatchFieldId;
import org.onosproject.net.pi.model.PiTableId;
import org.onosproject.net.pi.runtime.PiAction;
import org.onosproject.net.pi.runtime.PiActionParam;
import org.onosproject.net.topology.TopologyService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import bjtu.pinet.common.ParsingResult;
import bjtu.pinet.common.PinetEtherType;
import bjtu.pinet.common.pinetpacket.HttpPacket;
import bjtu.pinet.common.pinetpacket.IPv4Packet;
import bjtu.pinet.common.pinetpacket.HttpResponse;
import bjtu.pinet.dto.DecisionResultObject;
import bjtu.pinet.service.ModeDecisionService;
import bjtu.pinet.service.ModeDecisionServiceLocalImpl;
import bjtu.pinet.utils.IPUtils;

import java.nio.ByteBuffer;

/**
 * Skeletal ONOS application component.
 */
@Component(immediate = true)
public class APPIPv4Plus {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private ApplicationId appId;
    private static final String APP_NAME = "ipv4plus";
    private static final int FLOW_RULE_PRIORITY = 100;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    private CoreService coreService;
    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    private DeviceService deviceService;
    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    private FlowRuleService flowRuleService;
    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    private TopologyService topologyService;
    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    private HostService hostService;
    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    private PacketService packetService;

    private DeviceListener deviceListener;
    private PacketProcessor packetProcessor;

    @Activate
    protected void activate() {
        log.info("------------------ ipv4plus start --------------------");
        deviceListener = new DeviceListenerServiceImpl();
        packetProcessor = new PacketProcessorServiceImpl();
        appId = coreService.registerApplication(APP_NAME);
        deviceService.addListener(deviceListener);
        packetService.addProcessor(packetProcessor,2);

    }

    @Deactivate
    protected void deactivate() {
        log.info("------------------ ipv4plus stop --------------------");
        deviceService.removeListener(deviceListener);
    }

    public class DeviceListenerServiceImpl implements DeviceListener {

        @Override
        public void event(DeviceEvent event) {
            Device device = event.subject();
            String nodeName = device.id().toString();

            log.info("-------------- device --------------------" + device.toString());

            switch (event.type()) {
                case DEVICE_AVAILABILITY_CHANGED:
                    log.info("-------------- DEVICE_AVAILABILITY_CHANGED --------------------");
                    insertStaticFlows();
                    log.info("-------------- Static IPv4 Flows Inserted -------------");
                    break;
                case DEVICE_ADDED:
                    log.info("-------------- DEVICE_ADDED -------------");
                    insertStaticFlows();
                    log.info("-------------- Static IPv4 Flows Inserted -------------");
                    break;
                case DEVICE_UPDATED:
                    log.info("-------------- DEVICE_UPDATED -------------");
                    insertStaticFlows();
                    log.info("-------------- Static IPv4 Flows Inserted -------------");
                    break;
                case DEVICE_REMOVED:
                    log.info("-------------- DEVICE_REMOVED -------------");
                    break;
                default:
                    log.info("-------------- DEVICE_NOT_CHANGED -------------");
            }
        }
    }

    public void insertStaticFlows() {

        insertipv4ForwardRule(DeviceId.deviceId("device:152"), Ip4Address.valueOf("172.18.100.2"), PortNumber.fromString("1"), MacAddress.valueOf("f8:0f:41:f6:d8:6b"));
        insertipv4ForwardRule(DeviceId.deviceId("device:152"), Ip4Address.valueOf("172.18.106.2"), PortNumber.fromString("2"), MacAddress.valueOf("00:1b:21:4a:f2:6d"));
        insertipv4ForwardRule(DeviceId.deviceId("device:121"), Ip4Address.valueOf("172.18.100.2"), PortNumber.fromString("1"), MacAddress.valueOf("00:25:90:af:d8:9b"));
        insertipv4ForwardRule(DeviceId.deviceId("device:121"), Ip4Address.valueOf("172.18.106.2"), PortNumber.fromString("3"), MacAddress.valueOf("34:48:ed:f9:03:90"));
        insertipv4ForwardRule(DeviceId.deviceId("device:110"), Ip4Address.valueOf("172.18.100.2"), PortNumber.fromString("1"), MacAddress.valueOf("00:1b:21:4a:f2:6c"));
        insertipv4ForwardRule(DeviceId.deviceId("device:110"), Ip4Address.valueOf("172.18.106.2"), PortNumber.fromString("2"), MacAddress.valueOf("00:25:90:af:d8:91"));


        //long insertFlowsStart = System.nanoTime();
        //S1
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s1"), Ip4Address.valueOf("172.16.1.2"), PortNumber.fromString("0"), MacAddress.valueOf("08:00:00:00:01:11"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s1"), Ip4Address.valueOf("172.16.2.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:02:22"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s1"), Ip4Address.valueOf("172.16.3.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:03:33"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s1"), Ip4Address.valueOf("172.16.4.2"), PortNumber.fromString("2"), MacAddress.valueOf("08:00:00:00:04:44"));
        //S2
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s2"), Ip4Address.valueOf("172.16.1.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:01:11"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s2"), Ip4Address.valueOf("172.16.2.2"), PortNumber.fromString("0"), MacAddress.valueOf("08:00:00:00:02:22"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s2"), Ip4Address.valueOf("172.16.3.2"), PortNumber.fromString("2"), MacAddress.valueOf("08:00:00:00:03:33"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s2"), Ip4Address.valueOf("172.16.4.2"), PortNumber.fromString("3"), MacAddress.valueOf("08:00:00:00:04:44"));
        //S3
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s3"), Ip4Address.valueOf("172.16.1.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:01:11"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s3"), Ip4Address.valueOf("172.16.2.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:02:22"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s3"), Ip4Address.valueOf("172.16.3.2"), PortNumber.fromString("0"), MacAddress.valueOf("08:00:00:00:03:33"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s3"), Ip4Address.valueOf("172.16.4.2"), PortNumber.fromString("2"), MacAddress.valueOf("08:00:00:00:04:44"));
        //S4
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s4"), Ip4Address.valueOf("172.16.1.2"), PortNumber.fromString("0"), MacAddress.valueOf("08:00:00:00:01:11"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s4"), Ip4Address.valueOf("172.16.2.2"), PortNumber.fromString("2"), MacAddress.valueOf("08:00:00:00:02:22"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s4"), Ip4Address.valueOf("172.16.3.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:03:33"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s4"), Ip4Address.valueOf("172.16.4.2"), PortNumber.fromString("3"), MacAddress.valueOf("08:00:00:00:04:44"));
        //S5
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s5"), Ip4Address.valueOf("172.16.1.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:01:11"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s5"), Ip4Address.valueOf("172.16.2.2"), PortNumber.fromString("0"), MacAddress.valueOf("08:00:00:00:02:22"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s5"), Ip4Address.valueOf("172.16.3.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:03:33"));
        //insertipv4ForwardRule(DeviceId.deviceId("device:bmv2:s5"), Ip4Address.valueOf("172.16.4.2"), PortNumber.fromString("1"), MacAddress.valueOf("08:00:00:00:04:44"));

        //long insertFlowsEnd = System.nanoTime();
        log.info("-------------- ipv4 流规则生成与下发模块接收处理 ---------------");
    }

    /**
     * Generates and insert a flow rule to perform the tunnel FORWARD/EGRESS
     * function for the given switch, output port address and tunnel ID.
     *
     * @param switchId   switch ID
     * @param outPort    output port where to forward tunneled packets
     * @param ethdstAddr tunnel ID
     * @param dstIpAddr  if true, perform tunnel egress action, otherwise forward
     *                   packet as is to port
     */
    public void insertipv4ForwardRule(DeviceId switchId,
                                       Ip4Address dstIpAddr,
                                       PortNumber outPort,
                                       MacAddress ethdstAddr) {

        PiTableId ipv4ForwardTableId = PiTableId.of("MyIngress.ipv4_lpm");

        // Exact match on tun_id
        final PiMatchFieldId ipDestMatchFieldId = PiMatchFieldId.of("hdr.ipv4.dst_Addr");
        final PiCriterion match = PiCriterion.builder()
                .matchLpm(ipDestMatchFieldId, dstIpAddr.toOctets(), 32)
                .build();

        // Action depend on isEgress parameter.
        // if true, perform tunnel egress action on the given outPort, otherwise
        // simply forward packet as is (set_out_port action).
        final PiActionParamId portParamId = PiActionParamId.of("port");
        final PiActionParam portParam = new PiActionParam(portParamId, (short) outPort.toLong());
        final PiActionParamId ethdst_ParamId = PiActionParamId.of("dstAddr");
        final PiActionParam ethdstParam = new PiActionParam(ethdst_ParamId, ethdstAddr.toBytes());


        final PiActionId ingressActionId = PiActionId.of("MyIngress.ipv4_forward");
        final PiAction action = PiAction.builder()
                .withId(ingressActionId)
                .withParameter(ethdstParam)
                .withParameter(portParam)
                .build();

        log.info("Inserting rule on switch {}: table={}, match={}, action={}",
                switchId, ipv4ForwardTableId, match, action);

        insertPiFlowRule(switchId, ipv4ForwardTableId, match, action);
    }

    /**
     * Inserts a flow rule in the system that using a PI criterion and action.
     *
     * @param switchId    switch ID
     * @param tableId     table ID
     * @param piCriterion PI criterion
     * @param piAction    PI action
     */
    private void insertPiFlowRule(DeviceId switchId, PiTableId tableId,
                                  PiCriterion piCriterion, PiAction piAction) {

        FlowRule rule = DefaultFlowRule.builder()
                .forDevice(switchId)
                .forTable(tableId)
                .fromApp(appId)
                .withPriority(FLOW_RULE_PRIORITY)
                .makePermanent()
                .withSelector(DefaultTrafficSelector.builder()
                        .matchPi(piCriterion).build())
                .withTreatment(DefaultTrafficTreatment.builder()
                        .piTableAction(piAction).build())
                .build();
        try {
            flowRuleService.applyFlowRules(rule);
        }catch (Exception e){
            log.info("-------------- flowRuleService failure---------------" + rule.toString());
        }

    }

    /*-------------------------------------------------
                        IP 2 NDN
     --------------------------------------------------*/

    public class PacketProcessorServiceImpl implements PacketProcessor {
        private ParsingResult parsingResult;
        //@Reference(cardinality = ReferenceCardinality.MANDATORY)
        //protected PacketService packetService;
        private ModeDecisionService modeDecisionService = new ModeDecisionServiceLocalImpl();
        @Override
        public void process(PacketContext context) {
            if (context==null) {
                return;
            }
            if (context.isHandled()) {
                return;
            }
            InboundPacket pkt = context.inPacket();
            ConnectPoint connectPoint = pkt.receivedFrom();
            Ethernet ethPkt = pkt.parsed();
            if (ethPkt == null) {
                return;
            }
            if (isControlPacket(ethPkt)) {
                return;
            }
            short etherType = ethPkt.getEtherType();
            HostId id = HostId.hostId(ethPkt.getDestinationMAC(), VlanId.vlanId(ethPkt.getVlanID()));
            // Do not process LLDP MAC address in any way.
            if (id.mac().isLldp()) {
                return;
            }
            //log.info("------------收到请求包--------------\n"+pkt.toString());
            log.info("------------收到请求包--------------\n"+ethPkt.toString());
            parsingResult = getParsingResult(etherType,ethPkt,connectPoint);
            log.info("------------解析数据包得到--------------\n"+parsingResult);
            if (parsingResult != null){
                insertStaticFlows();
                DecisionResultObject decisionResultObject = modeDecisionService.modeDecision(parsingResult);
                if(decisionResultObject.isSupport()){
                    log.info("-----------------------------------模态决策--------------------------------------\n"+decisionResultObject.toString());
                    //TODO do something from decisionResultObject.
                    reSendToTerminal(decisionResultObject);
                }
            }
        }

        private boolean isControlPacket(Ethernet eth) {
            short type = eth.getEtherType();
            return type == Ethernet.TYPE_LLDP || type == Ethernet.TYPE_BSN;
        }


        public ParsingResult getParsingResult(short etherType, Ethernet ethPkt,ConnectPoint connectPoint){
            IPacket iPacket = ethPkt.getPayload();
            ParsingResult parsingResult ;
            if(etherType == PinetEtherType.IPV4.getEtherType()){
                IPv4 iPv4 = (IPv4) iPacket;
                int  sourceAddress = iPv4.getSourceAddress();
                String sourceAddressString  = IPUtils.longToIP(IPUtils.toLong(sourceAddress));
                int  destinationAddress = iPv4.getDestinationAddress();
                String destinationAddressString  = IPUtils.longToIP(IPUtils.toLong(destinationAddress));
                int networkTypeId = PinetEtherType.IPV4.getNetworkTypeId();
                IPv4Packet iPv4Packet = new IPv4Packet(iPv4);
                iPv4Packet.setPayload(iPv4.getPayload());
                TCP tcpPacket  = (TCP)iPv4.getPayload();
                log.info("------------------------------------iPv4Packet包"+iPv4Packet.getPayload());
                log.info("------------------------------------tcp包"+tcpPacket.toString());
                HttpPacket httpPacket = HttpPacket.deserialize(tcpPacket.getPayload().serialize());
                log.info("------------------------------------http包"+httpPacket);
                parsingResult = new ParsingResult(networkTypeId,etherType,iPv4Packet,new ParsingResult.PrincipalsObject(sourceAddressString, destinationAddressString),connectPoint);
            }else{
                return null;
            }
            return parsingResult;
        }


        private void reSendToTerminal(DecisionResultObject decisionResultObject) {

            String receiveFromString = decisionResultObject.getReceiceFrom();
            ConnectPoint receiveFrom = ConnectPoint.deviceConnectPoint(receiveFromString);
            log.info("-----------------------ConnectPoint-----------------------" + receiveFrom);
            IPv4Packet iPv4Packet = (IPv4Packet) decisionResultObject.getPacketParsingResult();
            log.info("-----------------------iPv4Packet-----------------------" + iPv4Packet);
            IPv4 iPv4Pkt = new IPv4().setDestinationAddress(iPv4Packet.getSourceAddress()).setSourceAddress(iPv4Packet.getDestinationAddress());
            HttpResponse httpResponse = new HttpResponse("GET", decisionResultObject.getDecisionPrincipalsObject().toString());
            TCP tcpPacket = new TCP();
            tcpPacket.setPayload(httpResponse);
            iPv4Pkt.setPayload(tcpPacket);
            log.info("-----------------------tcpPacket-----------------------" + tcpPacket);
            Ethernet ethPkt = new Ethernet();
            ethPkt.setEtherType(Ethernet.TYPE_IPV4);
            ethPkt.setDestinationMACAddress("08:00:00:00:01:11");
            ethPkt.setSourceMACAddress("08:00:00:00:01:00");
            ethPkt.setPayload(iPv4Pkt);
            log.info("-----------------------packet_out数据包-----------------------" +
                    "\n" + ethPkt +
                    "\n" + iPv4Packet +
                    "\n" + tcpPacket +
                    "\n" + httpResponse +
                    "\n" + "please send NDN request");
            DefaultOutboundPacket outPkt = new DefaultOutboundPacket(receiveFrom.deviceId(),
                    DefaultTrafficTreatment.builder().setOutput(receiveFrom.port()).build(), ByteBuffer.wrap(ethPkt.serialize()));
            log.info("-----------------------packet_out数据包-----------------------" + outPkt);
            packetService.emit(outPkt);
            log.info("pinetApp:***发送reply***");

        }


    }

}
